/**
 * Copyright 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule ReactMultiChild
 */

'use strict'

var ReactComponentEnvironment = require('./ReactComponentEnvironment')
var ReactMultiChildUpdateTypes = require('./ReactMultiChildUpdateTypes')

var ReactCurrentOwner = require('./ReactCurrentOwner')
var ReactReconciler = require('./ReactReconciler')
var ReactChildReconciler = require('./ReactChildReconciler')

var flattenChildren = require('./flattenChildren')
var invariant = require('fbjs/lib/invariant')

/**
 * Make an update for markup to be rendered and inserted at a supplied index.
 *
 * @param {string} markup Markup that renders into an element.
 * @param {number} toIndex Destination index.
 * @private
 */
function makeInsertMarkup(markup, afterNode, toIndex) {
  // NOTE: Null values reduce hidden classes.
  return {
    type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
    content: markup,
    fromIndex: null,
    fromNode: null,
    toIndex: toIndex,
    afterNode: afterNode
  }
}

/**
 * Make an update for moving an existing element to another index.
 *
 * @param {number} fromIndex Source index of the existing element.
 * @param {number} toIndex Destination index of the element.
 * @private
 */
function makeMove(child, afterNode, toIndex) {
  // NOTE: Null values reduce hidden classes.
  return {
    type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
    content: null,
    fromIndex: child._mountIndex,
    fromNode: ReactReconciler.getNativeNode(child),
    toIndex: toIndex,
    afterNode: afterNode
  }
}

/**
 * Make an update for removing an element at an index.
 *
 * @param {number} fromIndex Index of the element to remove.
 * @private
 */
function makeRemove(child, node) {
  // NOTE: Null values reduce hidden classes.
  return {
    type: ReactMultiChildUpdateTypes.REMOVE_NODE,
    content: null,
    fromIndex: child._mountIndex,
    fromNode: node,
    toIndex: null,
    afterNode: null
  }
}

/**
 * Make an update for setting the markup of a node.
 *
 * @param {string} markup Markup that renders into an element.
 * @private
 */
function makeSetMarkup(markup) {
  // NOTE: Null values reduce hidden classes.
  return {
    type: ReactMultiChildUpdateTypes.SET_MARKUP,
    content: markup,
    fromIndex: null,
    fromNode: null,
    toIndex: null,
    afterNode: null
  }
}

/**
 * Make an update for setting the text content.
 *
 * @param {string} textContent Text content to set.
 * @private
 */
function makeTextContent(textContent) {
  // NOTE: Null values reduce hidden classes.
  return {
    type: ReactMultiChildUpdateTypes.TEXT_CONTENT,
    content: textContent,
    fromIndex: null,
    fromNode: null,
    toIndex: null,
    afterNode: null
  }
}

/**
 * Push an update, if any, onto the queue. Creates a new queue if none is
 * passed and always returns the queue. Mutative.
 */
function enqueue(queue, update) {
  if (update) {
    queue = queue || []
    queue.push(update)
  }
  return queue
}

/**
 * Processes any enqueued updates.
 *
 * @private
 */
function processQueue(inst, updateQueue) {
  ReactComponentEnvironment.processChildrenUpdates(inst, updateQueue)
}

/**
 * ReactMultiChild are capable of reconciling multiple children.
 *
 * @class ReactMultiChild
 * @internal
 */
var ReactMultiChild = {
  /**
   * Provides common functionality for components that must reconcile multiple
   * children. This is used by `ReactDOMComponent` to mount, update, and
   * unmount child components.
   *
   * @lends {ReactMultiChild.prototype}
   */
  Mixin: {
    _reconcilerInstantiateChildren: function (
      nestedChildren,
      transaction,
      context
    ) {
      if (process.env.NODE_ENV !== 'production') {
        if (this._currentElement) {
          try {
            ReactCurrentOwner.current = this._currentElement._owner
            return ReactChildReconciler.instantiateChildren(
              nestedChildren,
              transaction,
              context
            )
          } finally {
            ReactCurrentOwner.current = null
          }
        }
      }
      return ReactChildReconciler.instantiateChildren(
        nestedChildren,
        transaction,
        context
      )
    },

    _reconcilerUpdateChildren: function (
      prevChildren,
      nextNestedChildrenElements,
      removedNodes,
      transaction,
      context
    ) {
      var nextChildren
      if (process.env.NODE_ENV !== 'production') {
        if (this._currentElement) {
          try {
            ReactCurrentOwner.current = this._currentElement._owner
            nextChildren = flattenChildren(nextNestedChildrenElements)
          } finally {
            ReactCurrentOwner.current = null
          }
          ReactChildReconciler.updateChildren(
            prevChildren,
            nextChildren,
            removedNodes,
            transaction,
            context
          )
          return nextChildren
        }
      }
      nextChildren = flattenChildren(nextNestedChildrenElements)
      ReactChildReconciler.updateChildren(
        prevChildren,
        nextChildren,
        removedNodes,
        transaction,
        context
      )
      return nextChildren
    },

    /**
     * Generates a "mount image" for each of the supplied children. In the case
     * of `ReactDOMComponent`, a mount image is a string of markup.
     *
     * @param {?object} nestedChildren Nested child maps.
     * @return {array} An array of mounted representations.
     * @internal
     */
    mountChildren: function (nestedChildren, transaction, context) {
      var children = this._reconcilerInstantiateChildren(
        nestedChildren,
        transaction,
        context
      )
      this._renderedChildren = children
      var mountImages = []
      var index = 0
      for (var name in children) {
        if (children.hasOwnProperty(name)) {
          var child = children[name]
          var mountImage = ReactReconciler.mountComponent(
            child,
            transaction,
            this,
            this._nativeContainerInfo,
            context
          )
          child._mountIndex = index++
          mountImages.push(mountImage)
        }
      }
      return mountImages
    },

    /**
     * Replaces any rendered children with a text content string.
     *
     * @param {string} nextContent String of content.
     * @internal
     */
    updateTextContent: function (nextContent) {
      var prevChildren = this._renderedChildren
      // Remove any rendered children.
      ReactChildReconciler.unmountChildren(prevChildren, false)
      for (var name in prevChildren) {
        if (prevChildren.hasOwnProperty(name)) {
          !false
            ? process.env.NODE_ENV !== 'production'
              ? invariant(
                  false,
                  'updateTextContent called on non-empty component.'
                )
              : invariant(false)
            : void 0
        }
      }
      // Set new text content.
      var updates = [makeTextContent(nextContent)]
      processQueue(this, updates)
    },

    /**
     * Replaces any rendered children with a markup string.
     *
     * @param {string} nextMarkup String of markup.
     * @internal
     */
    updateMarkup: function (nextMarkup) {
      var prevChildren = this._renderedChildren
      // Remove any rendered children.
      ReactChildReconciler.unmountChildren(prevChildren, false)
      for (var name in prevChildren) {
        if (prevChildren.hasOwnProperty(name)) {
          !false
            ? process.env.NODE_ENV !== 'production'
              ? invariant(
                  false,
                  'updateTextContent called on non-empty component.'
                )
              : invariant(false)
            : void 0
        }
      }
      var updates = [makeSetMarkup(nextMarkup)]
      processQueue(this, updates)
    },

    /**
     * Updates the rendered children with new children.
     *
     * @param {?object} nextNestedChildrenElements Nested child element maps.
     * @param {ReactReconcileTransaction} transaction
     * @internal
     */
    updateChildren: function (
      nextNestedChildrenElements,
      transaction,
      context
    ) {
      // Hook used by React ART
      this._updateChildren(nextNestedChildrenElements, transaction, context)
    },

    /**
     * @param {?object} nextNestedChildrenElements Nested child element maps.
     * @param {ReactReconcileTransaction} transaction
     * @final
     * @protected
     */
    _updateChildren: function (
      nextNestedChildrenElements,
      transaction,
      context
    ) {
      var prevChildren = this._renderedChildren
      var removedNodes = {}
      var nextChildren = this._reconcilerUpdateChildren(
        prevChildren,
        nextNestedChildrenElements,
        removedNodes,
        transaction,
        context
      )
      if (!nextChildren && !prevChildren) {
        return
      }
      var updates = null
      var name
      // `nextIndex` will increment for each child in `nextChildren`, but
      // `lastIndex` will be the last index visited in `prevChildren`.
      var lastIndex = 0
      var nextIndex = 0
      var lastPlacedNode = null
      for (name in nextChildren) {
        if (!nextChildren.hasOwnProperty(name)) {
          continue
        }
        var prevChild = prevChildren && prevChildren[name]
        var nextChild = nextChildren[name]
        if (prevChild === nextChild) {
          updates = enqueue(
            updates,
            this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex)
          )
          lastIndex = Math.max(prevChild._mountIndex, lastIndex)
          prevChild._mountIndex = nextIndex
        } else {
          if (prevChild) {
            // Update `lastIndex` before `_mountIndex` gets unset by unmounting.
            lastIndex = Math.max(prevChild._mountIndex, lastIndex)
            // The `removedNodes` loop below will actually remove the child.
          }
          // The child must be instantiated before it's mounted.
          updates = enqueue(
            updates,
            this._mountChildAtIndex(
              nextChild,
              lastPlacedNode,
              nextIndex,
              transaction,
              context
            )
          )
        }
        nextIndex++
        lastPlacedNode = ReactReconciler.getNativeNode(nextChild)
      }
      // Remove children that are no longer present.
      for (name in removedNodes) {
        if (removedNodes.hasOwnProperty(name)) {
          updates = enqueue(
            updates,
            this._unmountChild(prevChildren[name], removedNodes[name])
          )
        }
      }
      if (updates) {
        processQueue(this, updates)
      }
      this._renderedChildren = nextChildren
    },

    /**
     * Unmounts all rendered children. This should be used to clean up children
     * when this component is unmounted. It does not actually perform any
     * backend operations.
     *
     * @internal
     */
    unmountChildren: function (safely) {
      var renderedChildren = this._renderedChildren
      ReactChildReconciler.unmountChildren(renderedChildren, safely)
      this._renderedChildren = null
    },

    /**
     * Moves a child component to the supplied index.
     *
     * @param {ReactComponent} child Component to move.
     * @param {number} toIndex Destination index of the element.
     * @param {number} lastIndex Last index visited of the siblings of `child`.
     * @protected
     */
    moveChild: function (child, afterNode, toIndex, lastIndex) {
      // If the index of `child` is less than `lastIndex`, then it needs to
      // be moved. Otherwise, we do not need to move it because a child will be
      // inserted or moved before `child`.
      if (child._mountIndex < lastIndex) {
        return makeMove(child, afterNode, toIndex)
      }
    },

    /**
     * Creates a child component.
     *
     * @param {ReactComponent} child Component to create.
     * @param {string} mountImage Markup to insert.
     * @protected
     */
    createChild: function (child, afterNode, mountImage) {
      return makeInsertMarkup(mountImage, afterNode, child._mountIndex)
    },

    /**
     * Removes a child component.
     *
     * @param {ReactComponent} child Child to remove.
     * @protected
     */
    removeChild: function (child, node) {
      return makeRemove(child, node)
    },

    /**
     * Mounts a child with the supplied name.
     *
     * NOTE: This is part of `updateChildren` and is here for readability.
     *
     * @param {ReactComponent} child Component to mount.
     * @param {string} name Name of the child.
     * @param {number} index Index at which to insert the child.
     * @param {ReactReconcileTransaction} transaction
     * @private
     */
    _mountChildAtIndex: function (
      child,
      afterNode,
      index,
      transaction,
      context
    ) {
      var mountImage = ReactReconciler.mountComponent(
        child,
        transaction,
        this,
        this._nativeContainerInfo,
        context
      )
      child._mountIndex = index
      return this.createChild(child, afterNode, mountImage)
    },

    /**
     * Unmounts a rendered child.
     *
     * NOTE: This is part of `updateChildren` and is here for readability.
     *
     * @param {ReactComponent} child Component to unmount.
     * @private
     */
    _unmountChild: function (child, node) {
      var update = this.removeChild(child, node)
      child._mountIndex = null
      return update
    }
  }
}

module.exports = ReactMultiChild
